๐ ๊ณผ์  ์ค๋ช
์ด๋ฒ ๊ณผ์ ๋ ๊ธฐ์ ๊ณผ์ ๋ก ์ฃผ์ด์ง ํผ๊ทธ๋ง์ ๋์์ธ๊ณผ api๋ฅผ ์ด์ฉํด 2๊ฐ์ง ํ์ด์ง๋ฅผ ๊ตฌํํด์ผํ๋ค. ํ์ด์ง๋ ์ฐจ๋๋ฆฌ์คํธ๋ฅผ ๋ณด์ฌ์ฃผ๋ Homeํ์ด์ง, ํด๋น ์ฐจ๋์ ์ ๋ณด๋ฅผ ๋ณด์ฌ์ฃผ๋ detailํ์ด์ง๋ก ๊ตฌ์ฑ๋์ด ์์ผ๋ฉฐ, ์ถ๊ฐ ๊ตฌํ์ฌํญ์ผ๋ก ํ์ด์ค๋ถ๊ณผ ์นด์นด์คํก์ ๊ณต์ ์ ํด๋น ์ด๋ฏธ์ง์ ์ฐจ๋์ ๋ณด๋ค์ ๋ณด์ฌ์ค ์ ์์ด์ผํ๋ SEO๊ฐ ์์๋ค. ๊ณผ์  ์์ฒด๋ ์ ๋ฒ ๊ณผ์ ์ ํฌ๊ฒ ๋ค๋ฅธ ์ ์ด ์์ด์ ์์ํ๊ฒ ํ ์ ์์ ๊ฒ ๊ฐ์, ์ด๋ฒ ๊ธฐํ์ ๋ชจ๋ ๋ค๊ฐ์ด typescript๋ฅผ ๋์ ํด๋ณด๊ธฐ๋ก ํ๋ค.
UseReducer์ Context API
์ฒ์ ๊ณผ์ ๋ถํฐ ๊ณ์ํด์ ์ฌ์ฉํด์์ ์กฐ๊ธ์ ์ต์ํด์ง context API์ useReducer๋ฅผ ์ด๋ฒ์ ํจ๊ป ์ฌ์ฉํด๋ณด์๋ค. ๋ค๋ฅธ ํ์ ์ ๋ฒ๊ณผ์ ์ ์ฝ๋๋ค๊ณผ Velopert๋์ ๊ธ์ ์ฐธ๊ณ ํด์ ์ฝ๋๋ฅผ ๊ตฌ์ฑํ๋ค.
UseReducer
useReducer๋ ์ค์ฒฉ๋ ์ํ๋ ์ฌ๋ฌ๊ฐ์ง ์ํ๋ฅผ ํ๋์ ์ค๋ธ์ ํธ๋ก ๋ฌถ์ด์ ๊ด๋ฆฌํ ๋ ๋ฑ, ๋ณต์กํ ์ํ๊ด๋ฆฌ ๋ก์ง์ ๊ฐ๋จํ๊ฒ ์ฒ๋ฆฌํ ์ ์๋ react hook์ด๋ค. useReducer์ ๋ก์ง์ useState์ ์ ์ฌํ๊ฒ, ์ฐ๋ฆฌ๊ฐ ๊ด๋ฆฌํด์ผ ํ ์ํ๊ฐ ์๊ณ , ์ํ๋ฅผ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ ์ง๋ฅผ ๋ด๊ณ ์๋ action๊ณผ ์ ๋ฌ๋ฐ์ action์ ๋ฐ๋ผ ์ฒ๋ฆฌํด์ฃผ๋ dispatch๊ฐ ์๋ค.
const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case ActionType.SET_IS_LOADING:
      return {
        ...state,
        isLoading: action.isLoading,
      };
    ...
};
const [state, dispatch] = useReducer(reducer, initialState);
// const [state,setState]=useState()์ ์ ์ฌํด์ด๋ฒ ํ๋ก์ ํธ์์ reducer๋ฅผ ์ฌ์ฉํด๋ณธ ๋ถ๋ถ์ APIํธ์ถ์ ๋ฐ๋ฅธ error, isLoading, data๋ฅผ ํ๋๋ก ๊ด๋ฆฌํ๊ธฐ ์ํด, ์ ๋ฒ usefetch๋ก ๋ถ๋ฆฌํ๋ customHook์ useReducer๋ก ๋์ฒดํ๋ค.
type State = {
  isLoading: boolean;
  data: CarType[];
  error: string;
};
type Action =
  | { type: ActionType.SET_DATA; data: CarType[] }
  | { type: ActionType.SET_IS_LOADING; isLoading: boolean }
  | { type: ActionType.SET_ERROR; error: string };
const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case ActionType.SET_IS_LOADING:
      return {
        ...state,
        isLoading: action.isLoading,
      };
    case ActionType.SET_DATA:
      return {
        ...state,
        data: action.data,
      };
    case ActionType.SET_ERROR:
      return {
        ...state,
        error: action.error,
      };
    default:
      throw new Error('Unknown Action');
  }
};
export const CarsProvider = ({ children }: { children: React.ReactNode }) => {
  const [state, dispatch] = useReducer(reducer, initialState);
		...
};contextAPI
contextAPI๋ฅผ ๊ธฐ์กด์์ ์ฌ์ฉํ ๋๋ value์ ์ํ์ ํจ์๋ฅผ ๊ฐ์ด ๋ณด๋ด์ฃผ์์ง๋ง ์ด๋ฒ์ reducer๋ฅผ ์ฌ์ฉํ๋ฉด์ ์ํ์ dispatch ๋ ์ค ํ๋๋ง ํ์ํ ๋๊ฐ ์์ด, stateContext์ dispatchContext ๋ ๊ฐ์ง๋ก ๋๋์ด์ ๊ตฌ์ฑํ๋ค.
type State = {
  isLoading: boolean;
  data: CarType[];
  error: string;
};
type Action =
  | { type: ActionType.SET_DATA; data: CarType[] }
  | { type: ActionType.SET_IS_LOADING; isLoading: boolean }
  | { type: ActionType.SET_ERROR; error: string };
type CarsDistpatch = Dispatch<Action>;
export const CarsStateContext = createContext<State | null>(initialState);
export const CarsDispatchContext = createContext<CarsDistpatch | null>(null);
export const CarsProvider = ({ children }: { children: React.ReactNode }) => {
		...
    return (
    <CarsStateContext.Provider value={state}>
      <CarsDispatchContext.Provider value={dispatch}>
        {children}
      </CarsDispatchContext.Provider>
    </CarsStateContext.Provider>
  );
};useReducer์ contextAPI๋ฅผ ์ด์ฉํด์ ๋ณด๋ค ๊น๋ํ๊ฒ ์ํ๊ด๋ฆฌ๋ฅผ ํ ์ ์์๊ณ , reducer์์๋ง ์ํ๊ด๋ฆฌ ๋ก์ง์ ์ถ๊ฐํ๋ฉด ๋์ด์ ํ์ฅ์ฑ๋ ์ข์ ์ฅ์ ์ ๊ฐ๊ฒ ๋์๋ค.
enum ActionEnum {
  SET_IS_LOADING = "SET_IS_LOADING",
  SET_DATA = "SET_DATA",
  SET_ERROR = "SET_ERROR",
}
const App = () => {
  const dispatch = useCarsDispatch()
  const getList = useCallback(async () => {
    dispatch({ type: ActionType.SET_IS_LOADING, isLoading: true })
    try {
      const response = await carsAPI.getCars()
      if (response) {
        dispatch({ type: ActionType.SET_DATA, data: response?.payload })
      }
    } catch (e) {
      if (e instanceof HTTPError) {
        dispatch({ type: ActionType.SET_ERROR, error: e.errorMessage })
      }
      console.error(e)
    } finally {
      dispatch({ type: ActionType.SET_IS_LOADING, isLoading: false })
    }
  }, [dispatch])
  useEffect(() => {
    getList()
  }, [getList])
  return (
    <>
      <Header />
      <Outlet />
    </>
  )
}
export default AppcontextAPI๋ฅผ ์ด์ฉํ Filtering
์ด๋ฒ ๊ณผ์ ์์ ์ ์ฒด ์ฐจ๋์ค์์ category๋ฅผ ๋๋ฅด๋ฉด ํด๋น ์ฐจ๋์ ์ข ๋ฅ๋ง ๋ณด์ฌ์ค์ผํ๊ธฐ ๋๋ฌธ์ filtering ๋ก์ง๋ ํ์ํ๋ค. filtering์ ํ๊ธฐ ์ํด์๋ ๊ธฐ์กด์ ์ํ๋ฅผ ๊ฐ์ง๊ณ ์์ผ๋ฉด์ filterํ๊ณ ์ถ์ ์ฐจ๋๋ค๋ง ๋ณด์ฌ์ค์ผ ํ๊ธฐ ๋๋ฌธ์ ๊ธฐ์กด Reducer๋ก์ง์ ์ถ๊ฐํ์ง ์๊ณ ๋ฐ๋ก caterogryContext๋ฅผ ๋ง๋ค์ด ๊ด๋ฆฌํ๋ค.
//categoryContext.tsx
import { createContext, useState, useMemo } from "react"
import { CategoryType } from "types/CarsInterface"
const initialState = {
  category: "์ ์ฒด",
  setCategory: (category: CategoryType) => {},
}
export const CategoryContext = createContext(initialState)
export const CategoryProvider = ({
  children,
}: {
  children: React.ReactNode
}) => {
  const [category, setCategory] = useState<CategoryType>("์ ์ฒด")
  const value = useMemo(() => ({ category, setCategory }), [category])
  return (
    <CategoryContext.Provider value={value}>
      {children}
    </CategoryContext.Provider>
  )
}๊ฐ๊ฐ์ context API์ provider๋ ํ์ํ ๊ณณ์์ ๊ฐ์ธ ์ฃผ๋ คํ๋ค. ์ฐจ๋ ๋ชฉ๋ก์ด ์๋ค๋ฉด useParam์ผ๋ก ํด๋น ์ฐจ๋ ์ ๋ณด๋ ์ป์ ์ ์๊ธฐ ๋๋ฌธ์ ๋ฐ๋ก api๋ฅผ ํธ์ถํ์ง ์๊ณ ํ๋ฒ๋ง ํธ์ถํ๊ฒ ํ๊ธฐ ์ํด Router.jsx์์ carsProvider๋ฅผ ๊ฐ์ธ์ฃผ์๋ค. categoryProvider๋ category๋ฅผ updateํ๊ณ category๋ฅผ ์ด์ฉํด filtering๋ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์์ค๊ธฐ ์ํด categories์ carsList๊ฐ ์๋ home.tsx์์ ๊ฐ์ธ์ฃผ์๋ค.
//router.tsx
const Router = () => {
  return (
    <CarsProvider>
      <RouterProvider router={router} />
    </CarsProvider>
  )
}
//
const Home = () => {
  return (
    <CategoryProvider>
      <S.Section>
        <Categories />
        <CarList />
      </S.Section>
    </CategoryProvider>
  )
}
export default HomeCustom Hook
์ด๋ฒ ๊ณผ์ ๋ฅผ ํ๋ฉด์ ๊ฐ์ฅ ์ ๊ฒฝ์ผ๋ ํฌ์ธํธ์ค ํ๋๋ ์ปดํฌ๋ํธ์ ๋จ์ํ์๋ค. ๋ฉํ ๋๊ป์ ๊ฐ์ ํด์ฃผ์ ์ปดํฌ๋ํธ์ ์ถ์ํ์ ๋ํด ๋ง์ด ์๊ฐํ๋ฉด์ ๋๋๋ก์ด๋ฉด Component๊ฐ ๋ก์ง๊ณผ ๊ด๋ จ๋ ์ฝ๋๋ฅผ ๋ง์ด ๊ฐ์ง๊ณ ์์ง ์๊ณ , UI ๋ ๋๋ง ๋ก์ง๋ง์ ๊ฐ์ง๊ณ ์๊ฒ ๋ ธ๋ ฅํ๋ค. ๊ทธ๋ ๊ฒ ํ๊ธฐ ์ํด์๋ ์ค๋ณต๋๊ฑฐ๋ ์ฌ์ฉ๋๋ ๋ก์ง์ ๋ค๋ฅธ ํ์ผ๋ก ๋ณด๊ดํด์ผ ํ๊ณ , custom hook์ ์ ๊ทน์ ์ผ๋ก ์ฌ์ฉํ๋ค.
ํนํ home page์ carsList ์ปดํฌ๋ํธ๋ api๋ก ๋ฐ์์จ ์ฐจ๋๋ฆฌ์คํธ๋ฅผ ์นดํ ๊ณ ๋ฆฌ์ ๋ง๊ฒ ๋ณด์ฌ์ค์ผํ๋ค. ๋ด๋ถ์ carsContext๋ก๋ถํฐ ๋ฐ์์จ ๋ฐ์ดํฐ๋ฅผ filtering์ ํ ์๋ ์์ง๋ง ๋ก์ง์ ์ปดํฌ๋ํธ ๋ด๋ถ์ ๋จ๊ธฐ๊ณ ์ถ์ง ์์ customHook์ผ๋ก ๋ง๋ค์ด list๋ง ๋ฐ์์ฌ ์ ์๊ฒ ํ๋ค. useCarsValue ๋ด๋ถ์์ ํํฐ๋งํ๊ธฐ ๋๋ฌธ์ ์ปดํฌ๋ํธ๋ ์์ฒญ ๊ฐ์ํ๋ ๊ตฌ์กฐ๋ฅผ ๊ฐ์ง ์ ์์๋ค.
//useCars.tsx
export const useCarsState = () => {
  const state = useContext(CarsStateContext)
  if (!state) throw new Error("Can't find State Provider")
  return state
}
export const useCarsDispatch = () => {
  const dispatch = useContext(CarsDispatchContext)
  if (!dispatch) throw new Error("Can't find Dispatch Provider")
  return dispatch
}
export const useCarsValue = () => {
  const state = useCarsState()
  const { category } = useContext(CategoryContext)
  if (!state) throw new Error("Can't find StateProvider")
  if (!category) throw new Error("Can't find CategoryProvider")
  if (category === "์ ์ฒด") return state.data
  const filterd = state?.data.filter(
    car => SegmentEnum[car.attribute.segment] === category
  )
  return filterd
}
//carsList.tsx
import S from "./styles"
import CarItem from "../carItem/CarItem"
import { useCarsState, useCarsValue } from "../../hooks/useCars"
const CarList = () => {
  const { isLoading, error } = useCarsState()
  const data = useCarsValue()
  if (isLoading) {
    return (
      <S.Layout>
        <h3>๋ถ๋ฌ์ค๋ ์ค</h3>
      </S.Layout>
    )
  }
  if (error) {
    return (
      <S.Layout>
        <h3>{error}</h3>
      </S.Layout>
    )
  }
  if (data.length === 0) {
    return (
      <S.Layout>
        <h3>์ฐจ๋์ด ์์ต๋๋ค.</h3>
      </S.Layout>
    )
  }
  return (
    <ul>
      {data.map(car => (
        <CarItem key={car.id} {...car} />
      ))}
    </ul>
  )
}
export default CarListTypescript
typescript๋ ๊ณต๋ถ๋ฅผ ํด๋ ์ ์ฐ๋ ๋ฒ์ด ๋ฌด์์ธ์ง ๊ณ ๋ฏผ์ด ๋ง์ด ๋์๊ธฐ ๋๋ฌธ์ ๋น ๋ฅธ ๊ฐ๋ฐ์ ์ํด์ react๋ก ํ ํ์ ์ฒ์ฒํ typescript๋ก ๋ฐ๊ฟ์ผ์ง๋ผ๊ณ ์๊ฐํ์ง๋ง, ๊ณ์ ๋ฏธ๋ค์์๋ค. ์ด์ ๋ถํฐ๋ ๊ณ์ํด์ ์ฌ์ฉํ๋ฉด์ ๋ถ๋ชํ๋ฉด์ ๋ฐฐ์๋๊ฐ๊ธฐ๋ก ๋ง์๋จน์๋ค. ์ด๋ฒ ๊ณผ์ ๋ ๋๋ฌด ์น์ ํ๊ฒ ๊ณผ์ ์ api๋ฌธ์์ ๋ฐ์ดํฐ๋ง๋ค type๊น์ง ์์ธํ ์๋ ค์ฃผ๊ธฐ ๋๋ฌธ์ ๊ผญ ์ ์ฉํด๋ณด๊ณ ์ถ์ด ์ ๊ทน์ ์ผ๋ก ํ์ ์ ์ํ๋ค.
enum
enum์ ๋น์ทํ ์ญํ ์ ํ๋ ๋ณ์๋ค์ ๋ฌถ์์ผ๋ก ์ต๋ํ string์ด๋, number์ธ ์ํ๋ก ์๋ฏธ๋ฅผ ์ ์ ์๋ ์ฝ๋๋ฅผ ๋จ๊ธฐ์ง ์์ผ๋ ค ์ฌ์ฉํ๋ค. enum์ ์ฌ์ฉํ ๋ ์๋กญ๊ฒ ์๊ฒ๋ ์ ์ object์ ๊ฐ์ด ์ฌ์ฉ์ด ๊ฐ๋ฅํ๋ค๋ ์ ์ด์๋ค.
enum SegmentEnum {
  C = "์ํ",
  D = "์คํ",
  E = "๋ํ",
  SUV = "SUV",
}
type AttributeType = {
  segment: keyof typeof SegmentEnum
}segment์ type์ ์ ๋ฌํ ๋ segmentEnum์ค์ ํ๋๋ผ๊ณ ์๋ ค์ค ๋ keyof typeof๋ฅผ ์ด์ฉํ ์ ์์๊ณ ์ด๋ ๊ฒ ์ ๋ฌํด์ค enum์ value๊ฐ์ ์ฐพ์ ๋๋ custom Hook์์ key๊ฐ์ ์ ๋ฌํด์ ์ฐพ์ ์ ์์๋ค.
export const useCarsValue = () => {
  //	...
  const filterd = state?.data.filter(
    car => SegmentEnum[car.attribute.segment] === category
  )
  return filterd
}null/undefined error
null/undefined Error๋ ์๋ง ๊ฐ์ฅ ์์ฃผ ๋ง์ฃผํ๋ ์๋ฌ๊ฐ ์๋๊น ์ถ๋ค. ์กฐ๊ฑด๋ถ๋ก ๋ฐ์์ฌ ๊ฒฝ์ฐ๋ null๋ก ๋ฐ์์ฌ ๊ฒฝ์ฐ ํด๋น ์ค๋ธ์ ํธ์ property๊ฐ ์์ ์๋ ์๊ธฐ ๋๋ฌธ์ ์๋ฌ๋ฅผ ๋์ ธ์ค๋ค.
์๋ฌ๋ฅผ ๋ง๊ธฐ์ํด์๋ ํญ์ undefined์ด๋ null์ผ ๊ฒฝ์ฐ์ ์ฒ๋ฆฌํ ์ ์๋ ๋ก์ง์ ์ฒ๋ฆฌํ๋ฉด ๊ฐ๋จํ๊ฒ ํด๊ฒฐ์ด ๊ฐ๋ฅํ๋ค.
const Detail = () => {
  const { id } = useParams()
  const car = data.find(item => item.id === +id)
  //	...
  if (!car) {
    return (
      <S.Layout>
        <h3>url์ ํ์ธํด์ฃผ์ธ์</h3>
      </S.Layout>
    )
  }
  const { amount, attribute, startDate, insurance, additionalProducts } = car
  // ..
}
export default DetailCRA์์์ SEO ๋ฌธ์  ํด๊ฒฐ
์ด๋ฒ๊ณผ์ ๋ฅผ ํ ๋ CRA์์ ๊ฐ๋จํ๊ฒ react-helmet์ ์ด์ฉํ๋ฉด SEO๋ฅผ ํด๊ฒฐํ ์ ์์ ๊ฒ์ด๋ ์๊ฐ์ CRA๋ฅผ ์ด์ฉํด์ ์งํํ๋ค. ํ์ง๋ง ๋ง์ฃผํ ๋ฌธ์ ๋ค์ด ๋ง์๋๋ฐ ๋ฌธ์ ํด๊ฒฐ๊ณผ์ ์ ์ ๋ฆฌํด๋ณด๊ณ ์ ํ๋ค.
SEO ๊ด์ ์์์ CSR๊ณผ SSR
CSR์ client์์ ํ๋ฉด์ ๋ ๋๋งํ๋ ๋ฐฉ์์ผ๋ก, ์๋ฒ์์ ๋ฐ์ ํ๋์ ๋นํ์ด์ง index.html์ ๋์ ์ผ๋ก html ์์๋ฅผ ๋ง๋๋ javascript์ ๋ฐ์ ํ๋ฒ์ ๋ณด์ฌ์ค๋ค. ๊ทธ๋ก์ธํด ํ๋ฉด์ด ๋ณด์๊ณผ ๋์์ interactiveํ ํ์ด์ง๋ฅผ ๋ง๋ค ์ ์๋ ์ฅ์ ์ด ์๋ค. ๋ด๊ฐ ์์ฃผ ์ฌ์ฉํ๋ CRA (create-react-app)๋ ๊ฐํธํ๊ฒ CSR (client-side-rendering)์ด ๊ฐ๋ฅํ ํจํค์ง์ด์ง๋ง CSR์ ํน์ฑ์ผ๋ก SEO์๋ ์ทจ์ฝํ ๋จ์ ์ ๊ฐ์ง๋ค.
๊ทธ์ ๋ฐํด SSR (server-side-rendering) ์ ์๋ฒ์์ ์ ์ ์ธ ํ์ด์ง๋ฅผ ๋จผ์  ๋ง๋ค์ด ๋ ๋๋งํด์ฃผ๊ธฐ ๋๋ฌธ์ ์ด๊ธฐ ๋ ๋๋ง ์๋๊ฐ ๋น ๋ฅด๊ณ ๊ฒ์์์ง๊ณผ ๊ฐ์ ๋ด์ด ๋ณด์์ ๋ ํด๋น ๋ด์ฉ๋ค์ ๋ณผ ์ ์๊ธฐ ๋๋ฌธ์ SEO์ ํฐ ์ฅ์ ์ ๊ฐ๊ณ ์๋ค. CSR์ ๋นํด ๋จผ์  ํ๋ฉด์ด ๋ณด์ด๊ณ ์ดํ์ javascript๊ฐ ์คํ๋๊ธฐ ๋๋ฌธ์ ux์ธก๋ฉด์์๋ ๋จ์ ์ ๊ฐ์ง ์ ์๋ค. ์ด๋ฒ ๊ณผ์ ๋ฅผ ์ํด์๋ SSR์ด ๋ ์ ํฉํ ๋ฐฉ์์ด์์ ๊ฒ์ด๋ ์๊ฐ์ด ๋๋ค.
๊ทธ๋ฌ๋ฉด ์ CRA๋ก ์งํํ์๊น?
React์์ SSR์ ํ๊ธฐ ์ํด์๋ Next.js๋ฅผ ์ฌ์ฉํ๋ฉด ๋๋ค. ํ์ง๋ง ์์ง ์ฌ์ฉํด๋ณธ ์ ์ด ์๊ณ , typescript์ ์ข๋ ์ด์ ์ ๋ง์ถฐ์ ๊ณต๋ถํ๋ค ๋ณด๋ ์๊ฐ์ด ๋ถ์กฑํด ์ฐ์ ์ด๋ป๊ฒ๋ CRA์์ ํด๊ฒฐํ ์ ์๋ ๋ฐฉ๋ฒ์ ์ฐพ์์ ์ ์ฉํด๋ณด์๋ค.
React-Helmet
react-helmet์ react ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก index.html์ head ๋ด์ฉ์ ๋์ ์ผ๋ก ๋ฐ๊ฟ ์ ์๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค. ๊ณผ์ ์ ํ์ํ ๋ด์ฉ๋ค์ ๊ฐ detail ํ์ด์ง์ ์ ๋ณด์ ๋ง๊ฒ head ๋ด์ฉ์ ๋ฐ๊พธ๊ธฐ ์ํด meta ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ ํ์ ์ ๋ณด๋ฅผ ๋ด์์ฃผ์๋ค. ๊ทธ๊ฒฐ๊ณผ ํ์ด์ง์์ ์๋ฐ๋์ด์๋ ๊ฒ์ ํ์ธํ ์ ์์๋ค.
import { Helmet } from "react-helmet-async"
const Meta = ({ attribute, amount, id }: MetaProps) => {
  const { brand, name, imageUrl } = attribute
  return (
    <Helmet>
      <title>{`${brand} ${name}`}</title>
      <meta name="description" content={`์ ${amount}์`} />
      <meta property="og:type" content="website" />
      <link href={imageUrl} />
      <meta property="og:url" content={`${process.env.PUBLIC_URL}/${id}`} />
      <meta name="og:title" content={`${brand} ${name}`} />
      <meta name="og:description" content={`์ ${amount.toLocaleString()}์`} />
      <meta property="og:image" content={imageUrl} />
      <meta property="og:image:width" content={IMAGE_SIZE.width.toString()} />
      <meta property="og:image:height" content={IMAGE_SIZE.height.toString()} />
    </Helmet>
  )
}ํ์ง๋ง ๊ณต์ ๋ฅผ ํ ๋๋ ์ฌ์ ํ ์ด๊ธฐ index.html์ head๋ด์ฉ๋ง ๋ณด์ด๋ ๋ฌธ์ ์ ์ด ์กด์ฌํ๋ค. ์ด๋ฌํ ๋ฌธ์ ์ ์ head๋ด์ฉ์ด javascript๋ฅผ ์ด์ฉํด ๋์ ์ผ๋ก ๋ฐ๋์ง๋ง ๊ณต์ ๋ฅผ ํ์ ๋๋ ํ๋์ index.html์ ๋ด์ฉ์ด ๊ทธ๋๋ก ๋ฐ์๋์ด ์๊ธด ๋ฌธ์ ๋ก ์๊ฐ๋๋ค.
React-snap
react-snap์ react library๋ก react-router๋ก ๋ง๋ ๋์ ๋ผ์ฐํ ํ์ด์ง๋ง๋ค ์ ํฉํ htmlํ์ผ์ ๋ง๋ค์ด์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค. index.tsx๋ฅผ hydrate๋ฅผ ์ด์ฉํด client-sideํ์ด์ง๋ฅผ static HTML๋ก ๋ฐ๊ฟ์ค๋ค. ๋ฐ๊ฟ์ค ๊ฒฐ๊ณผ buildํด๋์ ๋ง๋ค์ด์ง ํ์ด์ง๋ค์ ํด๋์ index.html์ด ์๊ธด ๊ฑธ ๋ณผ ์ ์๋ค.
import { hydrate, render } from "react-dom"
const container = document.getElementById("root") as HTMLElement
const root = ReactDOM.createRoot(container)
if (container.hasChildNodes()) {
  ReactDOM.hydrateRoot(
    container,
    <React.StrictMode>
      <ThemeProvider theme={Theme}>
        <GlobalStyle />
        <Router />
      </ThemeProvider>
    </React.StrictMode>
  )
} else {
  root.render(
    <React.StrictMode>
      <ThemeProvider theme={Theme}>
        <GlobalStyle />
        <Router />
      </ThemeProvider>
    </React.StrictMode>
  )
}๋๊ฐ์ง ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ด์ฉํ ๋๋ถ์ ๋คํํ ๊ณต์ ์ ๋ด์ฉ๋ค์ด ๋ด์ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์๊ณ , react-snap์ ์ฐ๋ฉด์ ์๊ฒ๋ hydration์ด๋ ๋ฐฉ์์ด ์ค์ ๋ก SSR์ ์ํ ํ๋ ์์ํฌ๋ค Next.js์ Gatsby๊ฐ ์ด์ฉํ๋ ๋ฐฉ์์์ ์๊ฒ ๋์๋ค.
Axios
์ด๋ฒ ํ๋ก์ ํธ๋ฅผ ํ๋ฉด์ ์๋กญ๊ฒ ์๋ํ ๊ฒ์ fetch๋์ axios๋ฅผ ์ฌ์ฉํ๋ค๋ ์ ์ด์๋ค. fetch๋ก ์ก์ง ๋ชปํ๋ ์๋ฌ๋ค์ request์ response๋ก ๋๋ ์ ๋ฐ์ ์๋ฌํธ๋ค๋ง์ด ๋ ๊ฐํธํ์ผ๋ฉฐ, ํ์๋ถ์ด ์๋ ค์ฃผ์ axios์ interceptor๋ฅผ ์ด์ฉํ๋ฉด ๋ณด๋ด๊ธฐ ์ ์ ์ค์ ๋ค์ ์ถ๊ฐํ ์๋ ์์ด ๋ ์ ์ฉํ ๋ถ๋ถ์ด ๋ง์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ผ ์๊ฐ๋์๋ค.
์๋๋ ํ๋์ api๋ง ์ฌ์ฉํ ๋๋ class๋ก ์ฌ์ฉํ๋ฉด ์คํ๋ ค ๋ ๋ณต์กํ๊ฒ ๋ง๋ ๋ค๊ณ ์๊ฐํด์ ์ฌ์ฉํ์ง ์์์ง๋ง class๋ก ๋ถ๋ฆฌํ๋ฉด ์ข ๋ ์ ๋ฆฌ๊ฐ ๋ ์ ์๊ณ ํ์ฅ์ฑ์ด ๋๋ค๋ ์ฅ์ ์ด ์๊ณ , ์ ๋ฌ์ instance๋ฅผ ๋ง๋ค์ด์ ์ฌ์ฉํ๋ ์ ์ ๋ฐฐ์ธ ์๋ ์์๋ค.
import axios, { AxiosError, AxiosInstance } from "axios"
import { CarType, FuelEnum, SegmentEnum } from "types/CarsInterface"
import createAxiosInstance from "./axiosUtils"
import HTTPError from "../network/httpError"
const BASE_URL = "https://preonboarding.platdev.net/api/cars"
type GetCarsResponse = {
  payload: CarType[]
}
class CarsAPI {
  constructor(private axiosInstance: AxiosInstance) {}
  async getCars(fuelType?: FuelEnum, segment?: SegmentEnum) {
    try {
      const { data } = await this.axiosInstance.get<GetCarsResponse>(BASE_URL, {
        params: {
          fuelType,
          segment,
        },
      })
      return data
    } catch (error) {
      const { response } = error as unknown as AxiosError
      if (response) {
        throw new HTTPError(response?.status, response?.statusText)
      }
      throw new Error("Unknown Error")
    }
  }
}
const carsAPIinstance = createAxiosInstance(BASE_URL)
const carsAPI = new CarsAPI(carsAPIinstance)
export default carsAPI์๋ฌํธ๋ค๋ง
๊ธฐ์กด์ ์ฌ์ฉํ๋ ์๋ฌํธ๋ค๋ง์ ์ํ class๋ฅผ ์ด์ฉํด์ ํ์ํ ์๋ฌ ๋ฉ์์ง๋ฅผ ์ข ๋ ์ดํด๊ฐ ์๋๊ฒ ์์ ํ๊ณ , typescript์์ ์ ๊ณตํ๋ private, public์ ์ด์ฉํด ๋ณด๋ค ๊ฐ๋จํ๊ฒ constructorํจ์๋ฅผ ์ฌ์ฉํ ์ ์์๋ค. ์๋ฌ๋ฅผ ์์๋ฐ๊ธฐ ๋๋ฌธ์ message๋ public์ผ๋ก ์ฌ์ฉํด์ค์ผ ๋๋ค๋ ์ ์ ์๋กญ๊ฒ ์ ์ ์์๋ค.
export default class HTTPError extends Error {
  constructor(private statusCode: number, public message: string) {
    super(message)
  }
  get errorMessage() {
    switch (this.statusCode) {
      case 404:
        this.message = "์๋ชป๋ ์์ฒญ์
๋๋ค. url์ ํ์ธํด์ฃผ์ธ์"
        break
      default:
        throw new Error("Unknown Error")
    }
    return this.message
  }
}๐ข ๋ง์น๋ฉฐ
์ด๋ฒ ๊ธฐํ๋ฅผ ํตํด์ ์ ๊ธฐ์ ์ด SEO๋ฅผ ๊ณ ๋ คํ๋์ง, SEO๋ฅผ ํด๊ฒฐํ๋ ๋ฐฉ์์ผ๋ก SSR์ ์ด์ฉํด์ผํ๋ ์ด์ ๋ฅผ ์ฒด๊ฐํ ์ ์์๋ค. ๋ฌด์กฐ๊ฑด SSR์ด ์ข๊ณ ์ ํํ๋๊น ํด์ผ๋๋ค๋ ์๊ฐ๋ณด๋ค ์ด๋ค ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋์๊ณ ์ด๊ฒ์ ์ด๋์ ์ ์ฉํด์ผํ๋ ์ง ์๊ฒ๋์๊ณ , Next.js๋ฅผ ๊ณต๋ถํด๋ณด๊ณ ์ถ๋ค๋ ์์์ด ๋ ์๊ธฐ๋ ๊ณ๊ธฐ๊ฐ ๋์๋ค. ํ๋๋ฅผ ์๋ฒฝํ๊ฒ ํ๊ณ ๋ค์์ ํด์ผํ์ง ์๋ ์๊ฐ๋ ํ์ง๋ง, ์ด๋ฒ์ next.js๋ฅผ ๋ชฐ๋ผ์ ์ ์ฉ์ ๋ชปํ๋ค๋ ์์ฌ์์ด ์๊ฒจ, ๋ด๊ฐ ์ฌ์ฉํ ์ ์๋ ๋๊ตฌ๋ฅผ ๋์ด๋ ๊ณผ์ ๋ ํ์ํ๋ค๋ ๊นจ์ฐ์นจ๋ ์๊ฒผ๋ค.